package com.atguigu.gmall.index.service.impl;

import com.alibaba.fastjson.JSON;
import com.atguigu.gmall.common.bean.ResponseVo;
import com.atguigu.gmall.common.consts.GmallConsts;
import com.atguigu.gmall.index.annotation.GmallCache;
import com.atguigu.gmall.index.feign.GmallPmsClient;
import com.atguigu.gmall.index.service.IndexService;
import com.atguigu.gmall.pms.entity.CategoryEntity;
import lombok.val;
import org.apache.commons.lang.StringUtils;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.cache.annotation.CacheConfig;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.util.Arrays;
import java.util.List;
import java.util.Random;
import java.util.UUID;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

@Service
@CacheConfig(cacheNames = "indexService")
public class IndexServiceImpl implements IndexService {
    //RedisTemplate: 默认以序列化的形式保存对象，所以自定义类型需要实现序列化接口
    //改进RedisTemplate： 设置键和值的序列化器  通过RedisTemplate存值时，序列化器自动将值转为json字符串存入 读取时自动还原为对象
    //stringRedisTemplate: 以字符串的形式保存数据，如果是对象需要我们手动转为字符串(json)存储  只能存字符串(手动类型转换)
    @Resource
    RedisTemplate redisTemplate;
    @Resource
    StringRedisTemplate stringRedisTemplate;
    @Resource
    GmallPmsClient gmallPmsClient;
    //查询一级商品分类列表
    @Override
    public List<CategoryEntity> getLevelOneCategories() {
        ResponseVo<List<CategoryEntity>> responseVo = gmallPmsClient.parent(0L);
        return responseVo.getData();
    }
//    @CacheEvict
//    @Cacheable(value = "index:subs" , key = "#cid")

    @GmallCache(prefix = "index:subs:cache:" , lock = "index:subs:lock" ,
        timeout = 30 , random = 15)
    @Override
    public List<CategoryEntity> loadLevel23Cates(Long cid) {
        ResponseVo<List<CategoryEntity>> subsResponseVo = gmallPmsClient.subs(cid);
        List<CategoryEntity> subs = subsResponseVo.getData();
        return subs;
    }
    @Resource
    RedissonClient redissonClient;
    // 自动续期 可重入 防误删除 的分布式锁..
    @Override
    public void testLock() {
        //1、获取锁： 判断当前线程是否已经获取到锁 重入，如果不是的判断锁是否空闲，如果锁被使用获取锁失败
        //   如果锁空闲：获取锁成功 锁重入的次数为1  返回获取锁成功
        //   如果锁被使用：
        //               如果是当前线程的锁 代表重入 锁重入的次数+1  返回获取锁成功
        //               不是当前线程的锁  获取锁失败  返回失败
        String key = "lock";
        String uuid = UUID.randomUUID().toString();
        Long timeout = 30L;
        // set lock uuid;
        // hset lock uuid  count ;
        Boolean flag = getLock(key, uuid, timeout);
//        RLock lock = redissonClient.getLock("lock");
//        boolean flag = lock.tryLock();
        if(flag){
            try {
                //redis中存储num值，并发访问5000次，查看并发后的结果
                String numStr = stringRedisTemplate.opsForValue().get("num");
                if(StringUtils.isEmpty(numStr)){
                    return ;
                }
                Thread.sleep(100*1000);
//                reLockTest(key,uuid,timeout);
                //num++ 在设置到redis中
                int num = Integer.parseInt(numStr);
                stringRedisTemplate.opsForValue().set("num",num+"");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                //2、释放锁
                //释放重入锁： 如果是自己的锁 重入次数-1后为0代表释放成功 否则释放失败 ， 如果不是自己的锁释放失败
                //      如果是自己的锁： 重入次数-1
                //              次数为0：锁完全释放  删除lock锁 返回1
                //              次数>0: 锁未完全释放  返回2
                //      如果不是自己的锁： 释放失败 返回0
                unLock(key, uuid);
//                redissonClient.getLock("lock").unlock();
            }
        }else{
            try {
                Thread.sleep(100);
                testLock();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }


    }

    private void reLockTest(String key, String uuid, Long timeout) {
        Boolean flag = getLock(key, uuid, timeout);
        if(flag){
            try {
                System.out.println("reLockTest 执行了.....");
            } finally {
                unLock(key, uuid);
            }
        }else{
            try {
                Thread.sleep(100);
                reLockTest(key, uuid, timeout);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public void unLock(String key, String uuid) {
        String releaseScript = "if(redis.call('hexists' , KEYS[1] , ARGV[1]) == 0)" +
                " then" +
                "   return 0" +
                " elseif(redis.call('hincrby' , KEYS[1] , ARGV[1], -1) == 0)" +
                " then " +
                "  redis.call('del',KEYS[1])" +
                "  return 1" +
                " else " +
                "  return 2" +
                " end";
        Long result = stringRedisTemplate.execute(new DefaultRedisScript<>(releaseScript,Long.class)
             , Arrays.asList(key) , uuid);
        System.out.println("释放锁的结果："+ result);
    }

    public Boolean getLock(String key, String uuid, Long timeout) {
        String script = "if(redis.call('hexists',KEYS[1] , ARGV[1])==1) " + //重入获取锁
                " then " +
                "   redis.call('hincrby' , KEYS[1] , ARGV[1] , 1)" +
                "   return 2 " +
                " elseif(redis.call('exists' , KEYS[1])==0) " +//第一次获取锁
                " then" +
                "   redis.call('hset' , KEYS[1] ,ARGV[1] , 1)" +
                "   redis.call('expire' , KEYS[1] , ARGV[2])" +
                "   return 1 " +
                " else" +
                "   return 0 " +
                " end";
        Long flag = stringRedisTemplate.execute(new DefaultRedisScript<>(script, Long.class),
                Arrays.asList(key), uuid, timeout.toString());
        if(flag.longValue() == 1){
            String renewScript = "if(redis.call('hexists' , KEYS[1] , ARGV[1])==1)" +
                    " then " +
                    "  redis.call('expire' , KEYS[1] , ARGV[2])" +
                    "  return 1" +
                    " else" +
                    "  return 0" +
                    " end";
            //锁获取成功：如果第一次获取锁成功开启线程自动续期
            executor.execute(()->{
                while(stringRedisTemplate.execute(new DefaultRedisScript<>(renewScript, Boolean.class),
                        Arrays.asList(key), uuid, timeout + "")){
                    try {
                        //
                        Thread.sleep(timeout*2000/3);
                        System.out.println("续期方法执行了............");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
        }
        //1,2代表获取锁成功
        return flag>0;
    }


    //    @Override
    public void testLock1() {
        //1、获取锁：setnx lock uuid
        String key = "lock";
        long timeout = 30;
        String val = UUID.randomUUID().toString().replace("-","");
        Boolean flag = stringRedisTemplate.opsForValue()
                    .setIfAbsent(key,val,timeout, TimeUnit.SECONDS);
        //2、获取锁成功执行业务
        if(flag){
            //看门狗线程自动续期：为了避免 业务时间过长 锁过期
            //       如果锁还是当前线程的，一个线程每过锁过期时间的2/3 对锁进行续期
            // 判断锁是不是自己的，如果是更新锁的过期时间
            renew(key , val , timeout);
            try {
                //redis中存储num值，并发访问5000次，查看并发后的结果
                String numStr = stringRedisTemplate.opsForValue().get("num");
                if(StringUtils.isEmpty(numStr)){
                    return ;
                }
                //休眠60秒
                Thread.sleep(60*1000);
                //num++ 在设置到redis中
                int num = Integer.parseInt(numStr);
                num++;
                check(key,val,timeout);
                stringRedisTemplate.opsForValue().set("num",num+"");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                //3、执行完获取锁之后的业务：一定要释放锁 删除lock属性
                //防止误删除：判断锁是否是自己的 才删除
                //原子性问题： java代码多个redis操作 可能会被其他线程命令插队:借助LUA脚本实现
                //   redis支持LUA脚本，可以编写复杂的业务逻辑
//                if(val.equals(stringRedisTemplate.opsForValue().get(key))){
//                    stringRedisTemplate.delete(key);
//                }

                String script = "if(redis.call('get',KEYS[1]) == ARGV[1]) " +
                        " then " +
                        "     redis.call('del' , KEYS[1]) " +
                        "     return 1 " +
                        "  else " +
                        "     return 0 " +
                        "  end ";
                stringRedisTemplate.execute(new DefaultRedisScript<>(script,Boolean.class),
                        Arrays.asList(key) , val);
            }

        }else{
            //4、获取锁失败：等一会再次尝试获取锁
            //sof:
            try {
                Thread.sleep(50);
                this.testLock();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

        }

    }









    private void check(String key, String val, long timeout) {
        //1、获取锁
        Boolean flag = stringRedisTemplate.opsForValue()
                .setIfAbsent(key,val,timeout, TimeUnit.SECONDS);
        if(flag){
            //2、获取成功执行业务
            System.out.println("check方法执行了.....");
            //   释放锁
            String script = "if(redis.call('get',KEYS[1]) == ARGV[1]) " +
                    " then " +
                    "     redis.call('del' , KEYS[1]) " +
                    "     return 1 " +
                    "  else " +
                    "     return 0 " +
                    "  end ";
            stringRedisTemplate.execute(new DefaultRedisScript<>(script,Boolean.class),
                    Arrays.asList(key) , val);
        }else{
            //3、获取失败 等待后再次尝试获取锁
            try {
                Thread.sleep(100);
                check(key,val,timeout);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }


    }

    /*
        核心参数：
            nt corePoolSize, 核心线程数
          int maximumPoolSize, 最大线程数
          long keepAliveTime, 超过核心线程数的线程的存活时间
          TimeUnit unit, 单位
          BlockingQueue<Runnable> workQueue, 任务队列
          ThreadFactory threadFactory 线程工厂
          RejectedExecutionHandler handler 拒绝策略
        底层工作原理：
            执行新的任务时，先判断线程池线程数是否达到核心线程数
                如果未达到： 创建核心线程执行任务
                如果达到：
                    判断任务队列是否已满
                        如果未满存到任务队列
                        如果已满：
                                判断线程数是否达到最大线程数
                                    如果未达到：
                                            创建线程执行任务
                                    如果达到：
                                            使用拒绝策略处理
           线程池初始化时 线程数为0，最终运行稳定时线程数为核心线程数
     */
    ExecutorService executor = Executors.newSingleThreadExecutor();
    //看门狗线程自动续期：为了避免 业务时间过长 锁过期
    //       如果锁还是当前线程的，每过锁过期时间的2/3 对锁进行续期
    // 判断锁是不是自己的，如果是更新锁的过期时间
    private void renew(String key, String val, long timeout) {
        String script = "if(redis.call('get',KEYS[1])==ARGV[1]) " +
                " then " +
                "    redis.call('expire' , KEYS[1] ,ARGV[2])" +
                "    return 1 " +
                " else" +
                "    return 0" +
                " end ";
        executor.execute(()->{
            while( stringRedisTemplate.execute(new DefaultRedisScript<>(script,Boolean.class),
                    Arrays.asList(key) , val,timeout+"")){
                try {
                    //等待 2/3过期时间 后再次判断锁是否存在 如果存在 继续续期
                    Thread.sleep(timeout*2000/3);
                    System.out.println("续期..........");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

    }
//    @Override
//    public void testLock() {
//        //redis中存储num值，并发访问5000次，查看并发后的结果
//        String numStr = stringRedisTemplate.opsForValue().get("num");
//        if(StringUtils.isEmpty(numStr)){
//            return ;
//        }
//        //num++ 在设置到redis中
////        int num = Integer.parseInt(numStr);
////        num++;
//        stringRedisTemplate.opsForValue().increment("num");//set("num",num+"");
//    }

}
//    @Override
//    public List<CategoryEntity> loadLevel23Cates(Long cid) {
//        //1、先查询缓存中是否存在数据  :  index:subs:1
//        String key = GmallConsts.INDEX_SUBS_PREFIX + cid;
//        Object subsObj = redisTemplate.opsForValue().get(key);
//        if(subsObj!=null){
//            //2、如果查询到返回
//            return (List<CategoryEntity>) subsObj;
//        }
//        //3、没有没有缓存 查询数据库 存入到缓存中
//        //先根据一级分类id加载它的二级分类列表
//        //遍历所有的二级分类：根据二级分类id加载它的三级分类列表
//        // 最好只发起一次远程访问的请求得到所有数据： 需要在pms中再自定义一个接口
//        ResponseVo<List<CategoryEntity>> subsResponseVo = gmallPmsClient.subs(cid);
//        List<CategoryEntity> subs = subsResponseVo.getData();
//        redisTemplate.opsForValue().set(key ,subs);
//        return subs;
//    }
//    @Override
//    public List<CategoryEntity> loadLevel23Cates(Long cid) {
//        //1、先查询缓存中是否存在数据  :  index:subs:1
//        String key = GmallConsts.INDEX_SUBS_PREFIX + cid;
//        String subsJson = stringRedisTemplate.opsForValue().get(key);
//        if(StringUtils.isNotEmpty(subsJson)){
//            //2、如果查询到返回
//            List<CategoryEntity> categoryEntities = JSON.parseArray(subsJson, CategoryEntity.class);
//            return categoryEntities;
//        }
//        //3、没有没有缓存 查询数据库 存入到缓存中
//        //先根据一级分类id加载它的二级分类列表
//        //遍历所有的二级分类：根据二级分类id加载它的三级分类列表
//        // 最好只发起一次远程访问的请求得到所有数据： 需要在pms中再自定义一个接口
//        ResponseVo<List<CategoryEntity>> subsResponseVo = gmallPmsClient.subs(cid);
//        List<CategoryEntity> subs = subsResponseVo.getData();
//        stringRedisTemplate.opsForValue().set(key , JSON.toJSONString(subs));
//        return subs;
//    }
